from __future__ import print_function

import collections
import collections.abc
import pptx
from pptx.enum.shapes import PP_PLACEHOLDER, MSO_SHAPE_TYPE
from pptx.enum.dml import MSO_COLOR_TYPE, MSO_THEME_COLOR
from pptx.util import Length
from openpyxl import load_workbook
from io import BytesIO
import base64

import numpy as np

from PIL import Image
import os
from rapidfuzz import process as fuze_process
from operator import attrgetter

from tqdm import tqdm
from pptx2html.global_var import g
from pptx2html import global_var

import pptx2html.outputter as outputter

from pptx2html.columns import is_two_column_text, assign_shapes

from pptx2html.utils_optim import normal_pdf, fit_column_model


picture_count = 0

global out


# pptx type defination rules
def is_title(shape):
    if shape.is_placeholder and (
        shape.placeholder_format.type == PP_PLACEHOLDER.TITLE
        or shape.placeholder_format.type == PP_PLACEHOLDER.SUBTITLE
        or shape.placeholder_format.type == PP_PLACEHOLDER.VERTICAL_TITLE
        or shape.placeholder_format.type == PP_PLACEHOLDER.CENTER_TITLE
    ):
        return True
    return False


def is_text_block(shape):
    # print(shape.has_text_frame, shape.text, g.text_block_threshold)
    if shape.has_text_frame:
        if shape.is_placeholder and shape.placeholder_format.type == PP_PLACEHOLDER.BODY:
            return True
        if len(shape.text) > g.text_block_threshold:
            return True
    return False


def is_list_block(shape):
    levels = []
    for para in shape.text_frame.paragraphs:
        if para.level not in levels:
            levels.append(para.level)
        if para.level != 0 or len(levels) > 1:
            return True
    return False


def is_accent(font):
    if (
        font.underline
        or font.italic
        or (
            font.color.type == MSO_COLOR_TYPE.SCHEME
            and (
                font.color.theme_color == MSO_THEME_COLOR.ACCENT_1
                or font.color.theme_color == MSO_THEME_COLOR.ACCENT_2
                or font.color.theme_color == MSO_THEME_COLOR.ACCENT_3
                or font.color.theme_color == MSO_THEME_COLOR.ACCENT_4
                or font.color.theme_color == MSO_THEME_COLOR.ACCENT_5
                or font.color.theme_color == MSO_THEME_COLOR.ACCENT_6
            )
        )
    ):
        return True
    return False


def is_strong(font):
    if font.bold or (
        font.color.type == MSO_COLOR_TYPE.SCHEME
        and (font.color.theme_color == MSO_THEME_COLOR.DARK_1 or font.color.theme_color == MSO_THEME_COLOR.DARK_2)
    ):
        return True
    return False


def get_formatted_text(para):
    res = ""
    for run in para.runs:
        text = run.text
        if text == "":
            continue
        if not g.disable_escaping:
            text = out.get_escaped(text)
        try:
            if run.hyperlink.address:
                text = out.get_hyperlink(text, run.hyperlink.address)
        except:
            text = out.get_hyperlink(text, "error:ppt-link-parsing-issue")
        if is_accent(run.font):
            text = out.get_accent(text)
        elif is_strong(run.font):
            text = out.get_strong(text)
        if not g.disable_color:
            if run.font.color.type == MSO_COLOR_TYPE.RGB:
                text = out.get_colored(text, run.font.color.rgb)
        res += text
    return res.strip()


def process_title(shape, slide_idx):
    global out
    notes = []
    text = shape.text_frame.text.strip()
    if g.use_custom_title:
        res = fuze_process.extractOne(text, g.titles.keys(), score_cutoff=92)
        if not res:
            g.max_custom_title
            out.put_title(text, g.max_custom_title + 1)
        else:
            notes.append(f'Title in slide {slide_idx} "{text}" is converted to "{res[0]}" as specified in title file.')
            out.put_title(res[0], g.titles[res[0]])
    else:
        out.put_title(text, 1)

    return notes


def process_text_block(shape, _):
    # print("text block....", shape, is_list_block(shape))
    global out
    if is_list_block(shape):
        # generate list block
        if isinstance(out, outputter.html_outputter):
            out.write("<ul>")
        for para in shape.text_frame.paragraphs:
            if para.text.strip() == "":
                continue
            text = get_formatted_text(para)
            out.put_list(text, para.level)
        if isinstance(out, outputter.html_outputter):
            out.write("</ul>")
        out.write("\n")
    else:
        # generate paragraph block
        for para in shape.text_frame.paragraphs:
            if para.text.strip() == "":
                continue
            text = get_formatted_text(para)
            out.put_para(text)
    return []


def process_notes(text, _):
    global out
    if isinstance(out, outputter.quarto_outputter):

        out.put_para("::: {.notes}")
        out.put_para(text)
        out.put_para(":::")
    else:
        out.put_para("---")
        out.put_para(text)

    return []


def process_picture(shape, slide_idx, convert_image=None):
    """
    统一传入到外部变量处理
    convert_image 应当返回src内容
    """
    global out

    notes = []
    image = shape.image
    image_bytes = image.blob

    if convert_image is not None:
        src_res = convert_image(image_bytes)
        if src_res is not None and len(src_res):
            out.write(f'<img src="{src_res}" style="max-width:500px;" />\n')

        """
        bytes_stream = BytesIO(image_bytes)
        image = Image.open(bytes_stream)
        image.save("./test.jpg")
        """

    else:
        encoded_src = base64.b64encode(image_bytes).decode("utf-8")
        base64_str = "data:image/jpeg;base64," + encoded_src
        out.write(f'<img src="{base64_str}" style="max-width:500px;" />\n')

    return []


def process_table(shape, _):
    global out

    # for r in shape.table.rows:
    #   for c in r.cells:
    #     print(c.span_height, c.span_width, c.text.strip())

    table = [[(cell.text.strip(), cell.span_width, cell.span_height) for cell in row.cells] for row in shape.table.rows]
    if len(table) > 0:
        out.put_table(table)
    return []


def process_chart(shape, _):
    global out
    excel_bytes = shape.chart.part.chart_workbook.xlsx_part.blob
    wb = load_workbook(BytesIO(excel_bytes))
    for sheet_name in wb.sheetnames:
        ws = wb[sheet_name]

        empty_columns = []
        for column in ws.columns:
            if all(cell.value is None for cell in column):
                empty_columns.append(column[0].column)

        merge_cell_pts = {}
        skip_cells = []

        for merged_range in ws.merged_cells.ranges:
            rspan = merged_range.max_row - merged_range.min_row
            cspan = merged_range.max_col - merged_range.min_col
            start_pos = merged_range.coord.split(":")[0]
            merge_cell_pts[start_pos] = (rspan, cspan)

            for row in range(merged_range.min_row, merged_range.max_row + 1):
                for col in range(merged_range.min_col, merged_range.max_col + 1):
                    skip_cells.append((row, col))

        rows = list(ws.rows)
        tb = ""
        tb += f"<table><caption>{sheet_name}</caption>"

        for r in rows:
            tb += "<tr>"
            for i, c in enumerate(r):
                if i + 1 in empty_columns:
                    continue

                is_merge = c.coordinate in merge_cell_pts
                if not is_merge and (c.row, c.column) in skip_cells:
                    continue

                tb += "<td"
                if is_merge:
                    rspan, cspan = merge_cell_pts[c.coordinate]
                    if rspan > 0:
                        tb += f' rowspan="{rspan + 1}"'
                    if cspan > 0:
                        tb += f' colspan="{cspan + 1}"'

                if c.value is None:
                    tb += "></td>"
                else:
                    tb += f">{c.value}</td>"
            tb += "</tr>"
        tb += "</table>\n"
        out.write(tb)
    return []


def ungroup_shapes(shapes):
    res = []
    for shape in shapes:
        try:
            if shape.shape_type == MSO_SHAPE_TYPE.GROUP:
                res.extend(ungroup_shapes(shape.shapes))
            else:
                res.append(shape)
        except Exception as e:
            print(f"failed to load shape {shape}, skipped. error: {e}")
    return res


def process_shapes(current_shapes, slide_id, convert_image=None):
    local_notes = []
    for shape in current_shapes:
        # print(shape, is_text_block(shape))
        if is_title(shape):
            local_notes += process_title(shape, slide_id + 1)
        elif shape.has_chart:  # 插入的excel图标
            # print("slide_id has chat: ", slide_id)
            try:
                local_notes += process_chart(shape, slide_id + 1)
            except:
                continue

        elif is_text_block(shape):
            local_notes += process_text_block(shape, slide_id + 1)
        elif shape.shape_type == MSO_SHAPE_TYPE.PICTURE:
            try:
                local_notes += process_picture(shape, slide_id + 1, convert_image)  # 处理图片
            except AttributeError as e:
                print(f"Failed to process picture, skipped: {e}")
        elif shape.shape_type == MSO_SHAPE_TYPE.TABLE:  # 处理表格
            local_notes += process_table(shape, slide_id + 1)
        else:
            try:
                ph = shape.placeholder_format
                if ph.type == PP_PLACEHOLDER.OBJECT and hasattr(shape, "image") and getattr(shape, "image"):
                    local_notes += process_picture(shape, slide_id + 1, convert_image)
            except:
                pass

    return local_notes


# main
def parse(prs, outputer, convert_image=None):
    global out
    out = outputer
    notes = []

    # print("Starting conversion")

    # Adding inclusion of header for the first slide
    out.put_header()

    for idx, slide in enumerate(tqdm(prs.slides, desc="Converting slides")):
        # for idx, slide in enumerate(prs.slides):
        if g.page is not None and idx + 1 != g.page:
            continue
        shapes = []
        try:
            shapes = sorted(ungroup_shapes(slide.shapes), key=attrgetter("top", "left"))
        except:
            print("Bad shapes encountered in this slide. Please check or move them and try again.")
            print("shapes:")
            try:
                for sp in slide.shapes:
                    print(sp.shape_type)
                    print(sp.top, sp.left, sp.width, sp.height)
            except:
                print("failed to print all bad shapes.")

        process_shapes(shapes, idx + 1, convert_image)

        if not g.disable_notes and slide.has_notes_slide:
            text = slide.notes_slide.notes_text_frame.text
            if text:
                notes += process_notes(text, idx + 1)
        if idx < len(prs.slides) - 1 and g.enable_slides:
            out.put_para("\n---\n")
    out.close()

    if len(notes) > 0:
        print("Process finished with notice:")
        for note in notes:
            print(note)

    return out.get_output()


# 单页slide解析，
def parse_page(slide, outputer, idx=-1, convert_image=None):
    shapes = []
    notes = []

    global out
    out = outputer

    try:
        shapes = sorted(ungroup_shapes(slide.shapes), key=attrgetter("top", "left"))
    except:
        print("Bad shapes encountered in this slide. Please check or move them and try again.")
        print("shapes:")
        try:
            for sp in slide.shapes:
                print(sp.shape_type)
                print(sp.top, sp.left, sp.width, sp.height)
        except:
            print("failed to print all bad shapes.")

    process_shapes(shapes, idx + 1, convert_image=convert_image)

    if not g.disable_notes and slide.has_notes_slide and slide.notes_slide.notes_text_frame:
        text = slide.notes_slide.notes_text_frame.text
        if text:
            notes += process_notes(text, idx + 1)
    # if idx < len(prs.slides) - 1 and g.enable_slides:
    #   out.put_para("\n---\n")

    out.close()

    # if len(notes) > 0:
    #   print('Process finished with notice:')
    #   for note in notes:

    return out.get_output()


# Alternative parse function for qmd files
def parse_alt(prs, outputer):
    """
    该函数用不上
    """
    global out
    out = outputer
    notes = []

    slide_width = pptx.util.Length(prs.slide_width)
    slide_width_mm = slide_width.mm

    tout_vector = np.arange(1, slide_width_mm)

    print("Starting convertion to qmd file")

    # Adding inclusion of header for the first slide
    out.put_header()

    for idx, slide in enumerate(tqdm(prs.slides, desc="Converting slides")):
        # for idx, slide in enumerate(prs.slides):
        if g.page is not None and idx + 1 != g.page:
            continue
        shapes = []
        try:
            shapes = sorted(ungroup_shapes(slide.shapes), key=attrgetter("top", "left"))
        except:
            print("Bad shapes encountered in this slide. Please check or move them and try again.")
            print("shapes:")
            try:
                for sp in slide.shapes:
                    print(sp.shape_type)
                    print(sp.top, sp.left, sp.width, sp.height)
            except:
                print("failed to print all bad shapes.")

        pdf_modelo = is_two_column_text(slide)

        if pdf_modelo:
            # Model to infer number of columns

            salida = map(lambda mu, sigma: normal_pdf(t_vector, mu, sigma), pdf_modelo[0], pdf_modelo[1])
            sum_of_gaussian = np.mean(list(salida), axis=0)
            parameters = fit_column_model(t_vector, sum_of_gaussian)

            num_cols = int(len(parameters) / 2)

            dict_shapes = assign_shapes(slide, parameters, num_cols, slide_width_mm=slide_width_mm)
            print("Cantidad de elementos")
            print("pre: %d" % len(dict_shapes["shapes_pre"]))
            print("l: %d" % len(dict_shapes["shapes_l"]))
            print("c: %d" % len(dict_shapes["shapes_c"]))
            print("r: %d" % len(dict_shapes["shapes_r"]))

            notes.extend(process_shapes(dict_shapes["shapes_pre"], idx))
            if num_cols == 1:
                pass

            elif num_cols == 2:
                out.put_para(":::: {.columns}")

                out.put_para('::: {.column width="50%"}')
                notes.extend(process_shapes(dict_shapes["shapes_l"], idx))
                out.put_para(":::")
                out.put_para('::: {.column width="50%"}')
                notes.extend(process_shapes(dict_shapes["shapes_r"], idx))
                out.put_para(":::")
                out.put_para("::::")

            elif num_cols == 3:
                out.put_para(":::: {.columns}")

                if dict_shapes["shapes_l"]:
                    out.put_para('::: {.column width="33%"}')
                    notes.extend(process_shapes(dict_shapes["shapes_l"], idx))
                    out.put_para(":::")

                if dict_shapes["shapes_c"]:
                    out.put_para('::: {.column width="33%"}')
                    notes.extend(process_shapes(dict_shapes["shapes_c"], idx))
                    out.put_para(":::")

                if dict_shapes["shapes_r"]:
                    out.put_para('::: {.column width="33%"}')
                    notes.extend(process_shapes(dict_shapes["shapes_r"], idx))
                    out.put_para(":::")

                out.put_para("::::")

            else:
                raise (ValueError, "Number of columns not allowed")

        else:
            notes.extend(process_shapes(shapes, idx))

        if not g.disable_notes and slide.has_notes_slide:
            text = slide.notes_slide.notes_text_frame.text
            if text:
                notes += process_notes(text, idx + 1)
        if idx < len(prs.slides) - 1 and g.enable_slides:
            out.put_para("\n---\n")
    out.close()

    if len(notes) > 0:
        print("Process finished with notice:")
        for note in notes:
            print(note)
